require 'openssl'
require 'digest/sha1'

class Crypto
  @@DES = 1
  @@DES_CBC = 2
  @@DES_CBC_LP = 3
  @@DES_CBC_P = 4
  @@DES_ECB = 5
  @@DES_ECB_LP = 6
  @@DES_ECB_P = 7
  @@DES_MAC = 8
  @@DES_MAC_EMV = 9
  @@RSA = 10
  @@SHA_1 = 11
  
  def self.SHA_1
    return @@SHA_1  
  end
  
  def self.RSA
    return @@RSA
  end
  
  def self.DES
    return @@DES
  end

  def self.DES_CBC
    return @@DES_CBC
  end  
  
  def self.DES_CBC_LP
    return @@DES_CBC_LP
  end  

  def self.DES_CBC_P
    return @@DES_CBC_P
  end  

  def self.DES_ECB
    return @@DES_ECB
  end  

  def self.DES_ECB_LP
    return @@DES_ECB_LP
  end  

  def self.DES_ECB_P
    return @@DES_ECB_P
  end  

  def self.DES_MAC
    return @@DES_MAC
  end
  
  def self.DES_MAC_EMV
    return @@DES_MAC_EMV
  end
  
  def digest mech,bs_data
    data_for_digest = bs_data.toString(ASCII)
    case mech
    when @@SHA_1
      return ByteString.new(Digest::SHA1.hexdigest(data_for_digest),HEX)
    end
  end
  
  def sign key,mech,bs_data,bs_iv=nil
    data_for_sign = bs_data.toString(ASCII)
    key_value = key.getComponent(Key.DES).toString(ASCII)
    case mech
    when @@DES_MAC
      return ByteString.new(mac_for_personalization(key_value,data_for_sign,bs_iv),ASCII)
    when @@DES_MAC_EMV
      return ByteString.new(retail_mac(key_value,data_for_sign, bs_iv),ASCII)
    end      
  end
  #
  # is performed with single DES plus final triple DES MAC 
  # ccording to ISO 9797-1 as MAC Algorithm 3 with output 
  # transformation 3, without truncation, and with DES taking
  # the place of the block cipher. This is also known as the “Retail MAC”
  #  
  def retail_mac key, data , iv=nil
    cipher      = OpenSSL::Cipher::Cipher.new("des-cbc").encrypt
    cipher.key  = key[0,8]
    cipher.iv =  iv.toString(ASCII) unless iv.nil?

    data = pad(data) 

    single_data = data[0,data.length-8]
    # Single DES with XOR til the last block
    if single_data && single_data.length > 0
      mac_ = cipher.update(single_data)
      mac_ = mac_[mac_.length-8, 8]
    else # length of data was <= 8
      mac_ = "\x00"*8
    end

    triple_data = data[data.length-8, 8]
    mac = ""
    0.upto(7) { |i|
      mac << (mac_[i] ^ triple_data[i])
    }
    # Final Round of TDES
    cipher      = OpenSSL::Cipher::Cipher.new("des-ede").encrypt
    cipher.key  = key
    cipher.update(mac)
  end
  
  def mac_for_personalization key_value,data,bs_iv
    input = pad(data)
    cipher = OpenSSL::Cipher::Cipher.new("des-ede-cbc").encrypt
    cipher.key = key_value
    cipher.iv =  bs_iv.toString(ASCII) unless bs_iv.nil?
    mac = ""
    input.scan(/.{8}/m) {|block|
      mac = cipher.update block
    }
    mac
  end
  
  def generateRandom len
     return ByteString.new( Array.new(len){ rand(256) },HEX)
  end
  
  
  def deriveKey master_key,meth,data,derived_key
    algo = nil
    case meth
    when @@DES_CBC
      algo = "des-ede-cbc" 
    when @@DES_ECB
      algo = "des-ede" 
    end
    cipher = OpenSSL::Cipher::Cipher.new(algo).encrypt
    cipher.key = master_key.getComponent(Key.DES).toString(ASCII)
    enc_data = cipher.update(data.toString(ASCII))
    enc_data << cipher.final
    derived_key.setComponent( Key.DES, ByteString.new(enc_data, ASCII))
  end
  
  #
  #
  #
  def get_derive_key key,bs_public_data,bs_pad1,bs_pad2
       derive_key = Key.new()       
       data = bs_public_data.concat(bs_pad1).concat(bs_public_data).concat(bs_pad2)
       deriveKey(key, Crypto.DES_ECB, data, derive_key);
       return derive_key
  end
  
  #
  #
  #
  def get_session_key key,bs_pad,bs_sequence_counter,bs_tail
       session_key = Key.new()
       data = bs_pad.concat(bs_sequence_counter).concat(bs_tail)
       deriveKey(key, Crypto.DES_CBC, data, session_key);
       return session_key
  end
  #
  #
  #
  def decrypt key,meth,data,iv=nil
    algo = nil
    data_for_dec = data.toString(ASCII)
    if key.getType == Key.SECRET
      case meth
      when @@DES_CBC
          algo = "des-cbc" 
      when @@DES_CBC_LP,@@DES_CBC_P
          algo = "des-ede-cbc"
      when @@DES_ECB
          algo = "des-ede" 
      when @@DES_ECB_LP,@@DES_ECB_P
          algo = "des-ede"
      end 
      
      cipher = OpenSSL::Cipher::Cipher.new(algo).decrypt
      cipher.padding = 0
      cipher.key = key.getComponent(Key.DES).toString(ASCII)
      cipher.iv =  iv.toString(ASCII) unless iv.nil?
      dec_data = cipher.update(data_for_dec)
      dec_data << cipher.final
      dec_data = unpad(dec_data) if [@@DES_CBC_LP,@@DES_CBC_P,@@DES_ECB_LP,@@DES_ECB_P].include?(meth)
      return ByteString.new(dec_data, ASCII)
    elsif key.getType == Key.PUBLIC
      pub_key = OpenSSL::PKey::RSA.new
      pub_key.e = OpenSSL::BN.new(key.getComponent(Key.EXPONENT).toString(HEX))
      pub_key.n = OpenSSL::BN.new(key.getComponent(Key.MODULUS).toString(HEX).to_i(16).to_s())
      # 1 == PKCS1 padding, 3 = raw
      dec_data = pub_key.public_decrypt data_for_dec,3      
      return ByteString.new(dec_data, ASCII)
    end           
  end
  #
  #
  #
  def encrypt key,meth,data,iv=nil

    algo = nil
    data_for_enc = data.toString(ASCII)
    case meth
    when @@DES_CBC
        algo = "des-cbc" 
    when @@DES_CBC_LP,@@DES_CBC_P
        algo = "des-ede-cbc"
        data_for_enc = pad(data_for_enc)
    when @@DES_ECB
        algo = "des-ede" 
    when @@DES_ECB_LP,@@DES_ECB_P
        algo = "des-ede"
        data_for_enc = pad(data_for_enc)
    end
    
    cipher = OpenSSL::Cipher::Cipher.new(algo).encrypt
    cipher.padding = 0
           
    cipher.key = key.getComponent(Key.DES).toString(ASCII)
    cipher.iv =  iv.toString(ASCII) unless iv.nil?
    enc_data = cipher.update(data_for_enc) 
    enc_data << cipher.final
    return ByteString.new(enc_data, ASCII) 
  end 
  #
  # ISO7816-4Padding
  #
  def pad input
    input += "\x80"
    while (input.length % 8) != 0
      input << 0x00
    end
    input
  end 
  
  #
  #
  #
  def unpad input
    return input.sub(/\x80(\x00)*$/,"")
  end
  
  #
  #Encrypts the key specified by keyToWrap, using the key specified by wrapKey, 
  #and the algorithm specified by wrapMech
  #
  def wrap wrap_key,wrap_mech,key_to_wrap,key_result,iv=nil 
    key_to_wrap_data = key_to_wrap.getComponent(Key.DES)
    key_result.setWrapKey(key_to_wrap)
    key_result.setWrapMech(wrap_mech)
    key_result.setComponent(Key.DES, encrypt(wrap_key,wrap_mech,key_to_wrap_data,iv))   
  end
  
  #
  #
  #
  def unwrap key_to_unwrap,key_result
    key_to_unwrap_data = key_to_unwrap.getComponent(Key.DES)
    key_result.setComponent(Key.DES, descript(key_to_unwrap.getWrapKey,key_to_unwrap.getWrapMech,key_to_unwrap_data))   
  end
  
  #
  #
  #
  def kcv_of key
    data = ByteString.new("0000000000000000", HEX);
    kcv = encrypt key,Crypto.DES_ECB,data
    return kcv.left(3)
  end
end